#include <sstream>
#include "math.h"

#include "Cuba.h"
#include "IndexadoHashExtensible.h"
#include "DatoCuba.h"
#include "Registro.h"
#include "EstrategiaHashMod.h"
#include "funciones.h"
#include "RegistroMemoria.h"
#include "ClaveDatoCuba.h"

namespace Toolkit
{

//TODO: agregar que pasen por parametro el tamano inicial!
IndexadoHashExtensible::IndexadoHashExtensible(const unsigned long int tamanoComponente,
		     TipoSerializacion &tSerializacion):Indice(tamanoComponente, tSerializacion)
{
	this->profundidad = 0;
	this->estrategiaHash = new EstrategiaHashMod();
}

IndexadoHashExtensible::~IndexadoHashExtensible()
{
	delete estrategiaHash;
}


unsigned long int IndexadoHashExtensible::crearDireccion(const Clave &clave) const
{
	string resultadoFuncionHash = estrategiaHash->aplicar(clave);
	unsigned long int direccion = 0;

	for(unsigned int i = 0; i < profundidad; i++)
	{	//TODO: ver como es el tema de manejo de bit con strings/streamstrings
		direccion = direccion << 1;
		/*int lowbit = resultadoFuncionHash & 1; //obtiene el ultimo bit de resultadoFuncionHash
		direccion = direccion | lowbit; // agrega el ultimo bit de resultadoFuncionHash a direccion
		resultadoFuncionHash = resultadoFuncionHash >> 1; //resultadoFuncionHash pierde el bit de la derecha
		*/
	}

	return direccion;
}

void IndexadoHashExtensible::insertar(const Clave &clave, const unsigned long int posicionBloque)
{
	Cuba cubaObjetivo = buscarCuba(clave);

	if ( cubaObjetivo.buscar(clave) != NULL ) throw ViolacionUnicidadException();

	DatoCuba datoCuba(posicionBloque, estrategiaHash->aplicar(clave));
	RegistroMemoria registroTemp (this->tSerializacion);
	registroTemp.setDato(&datoCuba);

	if ( ! cubaObjetivo.agregar( registroTemp, clave ) ) {
		if ( cubaObjetivo.getProfundidad() == profundidad )
			expandir();

		Cuba nuevaCuba = dividirCuba(cubaObjetivo);
		agregarCuba(nuevaCuba);
		insertar( clave, posicionBloque);
	}
	else {
		modificarCuba(cubaObjetivo);
	}
}


void IndexadoHashExtensible::intentarCombinar(Cuba &cuba, unsigned long int direccionDirectorio)
{
	unsigned long int direccionCompanero = direccionDirectorio;
	bool tieneCompanero = buscarCompaneroCuba(cuba, direccionCompanero);

	/*
	 *  Si la Cuba tiene companero, intenta combinarse con este. Si logra combinarse, el directorio
	 *  se intenta contraer, en caso de lograrlo el metodo llama recursivamente al metodo
	 *  intentarCombinar porque existe la posibilidad de que la Cuba se combine de nuevo.
	 */
	if (tieneCompanero)
	{
		Cuba cubaCompanero = buscarCuba(direccionCompanero);
		bool exitoCombinar = cuba.combinar(cubaCompanero);

		if (exitoCombinar)
		{
			directorio[direccionCompanero] = directorio[direccionDirectorio];
			bool exitoContraer = contraer();

			if (exitoContraer)
			{
				direccionDirectorio = direccionDirectorio >> 1;
				intentarCombinar(cuba, direccionDirectorio);
			}
		}
	}
}

bool IndexadoHashExtensible::eliminar(const Clave &clave, unsigned long int &posicionBloque)
{
	Cuba cuba = buscarCuba(clave);
	Registro *registro= (Registro*)cuba.remover(clave);

	//si no existe la clave a eliminar
	if (registro == NULL)
	{
		posicionBloque = 0;
		return false;
	}

	unsigned long int direccionDirectorio = crearDireccion(clave);
	intentarCombinar(cuba, direccionDirectorio);

	DatoCuba *datoCuba = (DatoCuba*)registro->getDato();
	posicionBloque = datoCuba->getPosicionBloque();

	//TODO ? Persistir las modificaciones

	return true;
}

bool IndexadoHashExtensible::buscar(const Clave & clave, unsigned long int &posicionBloque) const
{
	Cuba cuba = buscarCuba(clave);
	Registro *registro = (Registro*)cuba.buscar(clave);

	if (registro == NULL)
	{
		posicionBloque = 0;
		return false;
	}

	DatoCuba *datoCuba = (DatoCuba*)registro->getDato();
	posicionBloque = datoCuba->getPosicionBloque();

	return true;
}


Cuba IndexadoHashExtensible::buscarCuba(const Clave &clave) const
{
	unsigned long int direccionDirectorio = crearDireccion(clave);
	return buscarCuba(direccionDirectorio);
}

Cuba IndexadoHashExtensible::buscarCuba(const unsigned long int direccionDirectorio) const
{
	unsigned long int posicionCuba = this->directorio[direccionDirectorio];
	stringstream cubaSerializada;
	almacenamientoCubas->recuperarDato(posicionCuba, cubaSerializada);

	Cuba cuba(tamanoComponente, tSerializacion);

	cuba.hidratar(cubaSerializada);

	return cuba;
}

void IndexadoHashExtensible::expandir()
{
	profundidad++;
	int cantidadElementos = (int)pow(2,profundidad);
	directorio.resize(cantidadElementos);

	int j = 0;
	for(int i = cantidadElementos/2 ; i > 0; i --)
	{
		directorio[cantidadElementos -1 -j] = directorio[i - 1];
		directorio[cantidadElementos -2 -j] = directorio[i - 1];
		j += 2;
	}
}

bool IndexadoHashExtensible::contraer()
{
	int cantidadElementos = (int)pow(2,profundidad);

	/*
	 * Recorro el directorio y verifico que todas las cubas
	 * esten direccionadas por lo menos por dos direcciones.
	 */
	for(int i = 0; i < cantidadElementos; i += 2)
	{
		if (directorio[i] != directorio[i+1])
		{
			return false;
		}
	}

	for(int i = 0; i < cantidadElementos/2; i++)
	{
		directorio[i] = directorio[2*i];
	}

	directorio.resize(cantidadElementos);
	profundidad--;

	return true;
}

//Analizar si el metodo va en la clase Cuba
bool IndexadoHashExtensible::buscarCompaneroCuba(const Cuba &cuba, unsigned long int &posicionCompanero) const
{
	if ((cuba.getProfundidad() == 0) || (cuba.getProfundidad() < profundidad))
	{
		posicionCompanero = 0;
		return false;
	}

	/**
	 * Aplico un xor a la direccion de la cuba, modificando el ultimo bit de esta, obteniendo
	 * asi la posicion de la cuba Companera.
	 */
	posicionCompanero = posicionCompanero ^ 1;

	return true;
}

//Analizar si el metodo va en la clase Cuba
void IndexadoHashExtensible::calcularRangoDirecciones(const Cuba &cuba, unsigned int &comienzo,
													  unsigned int &fin) const
{
	ClaveDatoCuba claveDeUnDatoCualquiera = ((DatoCuba*)((RegistroMemoria*)Cuba(cuba).pop())->getDato())->getClaveDelDato();
	int direccionCorrespondiente = crearDireccion(ClaveDatoCuba(claveDeUnDatoCualquiera));

	int diferenciaProfundidades = this->profundidad - cuba.getProfundidad();
	int cantidadRegistrosPorCuba = (int)pow(2,diferenciaProfundidades);
	comienzo = direccionCorrespondiente - ( direccionCorrespondiente % cantidadRegistrosPorCuba );
	fin = comienzo + cantidadRegistrosPorCuba;
}

//util?
void IndexadoHashExtensible::agregarCuba(const Cuba &cuba)
{
	stringstream cubaSerializada;
	cuba.serializar(cubaSerializada);
	unsigned long int posicionNuevaCuba = almacenamientoCubas->guardarDato(cubaSerializada);

	unsigned int inicio, fin;
	calcularRangoDirecciones(cuba, inicio, fin);

	// Recorro el directorio direccionando todas las posiciones correspondientes a la nueva cuba
	for (unsigned int i = inicio; i < fin; ++i)
	{
		directorio[i] = posicionNuevaCuba;
	}
}

void IndexadoHashExtensible::modificarCuba(const Cuba &cuba)
{
	unsigned int inicio, fin;
	calcularRangoDirecciones(cuba, inicio, fin);
	stringstream cubaSerializada;
	cuba.serializar(cubaSerializada);
	almacenamientoCubas->guardarDato(cubaSerializada, directorio[inicio]);
}

/* TODO: Analizar la posible combinacion con el metodo general "eliminar", y como se se dividen las responsabilidades
	Y con eso, faltaria agregar la actualizacion del directorio
*/
void IndexadoHashExtensible::eliminarCuba(const unsigned long int posicion)
{
	stringstream resultado;
	almacenamientoCubas->eliminarDato(posicion, resultado);


}

Cuba IndexadoHashExtensible::dividirCuba (Cuba & cubaLlena)
{
	cubaLlena.incrementarProfundidad();
	Cuba cubaNueva (cubaLlena);
	Cuba cubaTmp (cubaLlena); // Copio la cuba llena, asi puedo removerle elementos y tratar a la copia como una cola sin alterar el orden cada vez q remuevo

	ClaveDatoCuba claveDeUnDatoCualquiera = ((DatoCuba*)((RegistroMemoria*)Cuba(cubaLlena).pop())->getDato())->getClaveDelDato();
	int direccionCorrespondiente = crearDireccion(ClaveDatoCuba(claveDeUnDatoCualquiera));

	unsigned int diferenciaProfundidades = this->profundidad - cubaLlena.getProfundidad();
	unsigned int cantidadRegistrosPorCuba = (int)pow(2,diferenciaProfundidades);
	unsigned int registroInicial = direccionCorrespondiente - ( direccionCorrespondiente % (cantidadRegistrosPorCuba + 1) );

	while (cubaTmp.cantElementos() > 0)
	{
		RegistroMemoria* regTemp =  (RegistroMemoria*) cubaTmp.pop();
		ClaveDatoCuba claveTemp ( ((DatoCuba*)regTemp->getDato())->getClaveDelDato() );

		if ( crearDireccion( claveTemp ) < registroInicial + cantidadRegistrosPorCuba ) {
			cubaNueva.remover(claveTemp);
		}
		else {
			cubaLlena.remover(claveTemp);
		}

		delete regTemp;
	}
	return cubaNueva;
}

}
